// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package output

import (
	"bufio"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"chainmaker.org/chainmaker/smarteditor/container"
	"chainmaker.org/chainmaker/smarteditor/global"

	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/i18n"
	"chainmaker.org/chainmaker/smarteditor/session"
	"chainmaker.org/chainmaker/smarteditor/util"

	"github.com/88250/gulu"
)

// RealSdkGoRootSuffix real go root path
// such as:
// mock path: /usr/local/go
// real path: /usr/local/gocopy
const RealSdkGoRootSuffix = "copy"

// BuildHandler handles request of building.
func BuildHandler(w http.ResponseWriter, r *http.Request) {
	uid, err := session.GetUidFromSession(r)
	if err != nil {
		log.Errorf("build handler access forbidden. uid:%s, err:%v", uid, err)
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)
	//resource limit
	if conf.Wide.OpenLimit {
		if !container.SC.SystemResourcePool.Get() {
			log.Warnf("[BuildAllContractHandler]reach resource limit.")
			result.Code = -1
			result.Msg = "resource limit"
			return
		} else {
			defer container.SC.SystemResourcePool.Put()
		}
	}

	params, err := util.ReadArgs(r.Body)
	if err != nil {
		log.Errorf("buildhandler decode parameter failed. uid:%s, url:%s, err:%v", uid, r.URL, err)
		result.Code = -1
		return
	}

	sid := params["sid"].(string)
	filePath := params["file"].(string)
	code := params["code"].(string)

	if filePath == "" {
		log.Errorf("build handler file path is empty. uid:%s", uid)
		http.Error(w, "not found main.go", http.StatusForbidden)
		return
	}

	if gulu.Go.IsAPI(filePath) || util.BelongsGoPath(filePath) {
		result.Msg = "gopath or gosrc"
		return
	}

	if !session.CanAccess(uid, filePath) {
		log.Errorf("build handler access path forbidden. uid:%s, path:%s", uid, filePath)
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	err = ioutil.WriteFile(filePath, []byte(code), 0644)
	if nil != err {
		log.Errorf("build handler write data to file fiald. err:%v", err)
		result.Code = -1
		return
	}

	channelRet := map[string]interface{}{}
	//向前端推送消息。
	fileDirectory := filepath.Dir(filePath)
	userConfigInfo := conf.GetUser(uid)
	workspace := userConfigInfo.Workspace

	log.Debugf("build handler, build file path. sid:%s, workspace:%s, file path:%s", uid, workspace, filePath)

	if !util.IsContractRootDirectory(workspace, fileDirectory) {
		log.Errorf("build handler, file path is not contract root path, don't need build. uid:%s, workspace:%s, file path:%s", sid, filePath)
		result.Code = -1
		return
	}
	// 在这里加一下校验,如果是210的就不再执行了
	is210, checkErr := util.CheckContractIs210(fileDirectory)
	if checkErr != nil {
		log.Errorf("CheckContractIs210 %s failed %s", fileDirectory, checkErr.Error())
	}
	if _, err := session.GetOutputWS(sid); err == nil {
		msg := i18n.Get(userConfigInfo.Locale, "start-check").(string)
		if is210 {
			msg = msg + "检测到合约版本为v2.1.0+，请将IDE的长安链版本切换至v2.1.0+再进行操作"
		}
		channelRet["output"] = "<span class='start-build'>" + msg + "</span>\n"
		channelRet["cmd"] = "start-build"
		wsChannel, err := session.GetOutputWS(sid)
		if err == nil {
			err := wsChannel.WriteJSON(&channelRet)
			if err != nil {
				log.Warnf("build handler write start build faild. err:%v", err)
			} else {
				wsChannel.Refresh()
			}
		} else {
			log.Warnf("build handler write build data to channel, channel is nil")
		}
	}
	if is210 {
		return
	}

	if !gulu.File.IsExist(filepath.Join(fileDirectory, "go.mod")) {
		modStart := time.Now()
		output, err := util.RunGoModInit(uid, fileDirectory, workspace, os.Getenv("GOPATH"), os.Getenv("GOROOT")+RealSdkGoRootSuffix, conf.Docker)
		if err != nil {
			sendBuildErrorMessageToWs(uid, sid, output)
			log.Errorf("build handler init go mod fail. output:%s, err:%v", output, err)
			result.Code = -1
			return
		}
		//执行go get获取指定依赖
		//output, err = util.RunGoGet(uid, fileDirectory, workspace, conf.Docker)
		//if err != nil {
		//	sendBuildErrorMessageToWs(uid, sid, output)
		//	log.Errorf("build handler go get fail. output:%s, err:%v", output, err)
		//	result.Code = -1
		//	return
		//}

		log.Debugf("time cost: build handler, go mod. (cost:%s),output:%s", time.Since(modStart), output)
	}
	tidyStart := time.Now()
	output, err := util.RunGoModTidyInDirectoryDocker(fileDirectory, conf.Docker, os.Getenv("GOPATH"), os.Getenv("GOROOT")+RealSdkGoRootSuffix)
	if err != nil {
		sendBuildErrorMessageToWs(uid, sid, output)
		log.Errorf("build handler init go tidy fail. output:%s, err:%v", output, err)
		result.Code = -1
		return
	}
	log.Debugf("time cost: build handler, go tidy. (cost:%s),output:%s", time.Since(tidyStart), output)

	session.SendUpdateFileEvent(sid, filepath.Join(fileDirectory, util.GoModFile))
	session.SendUpdateFileEvent(sid, filepath.Join(fileDirectory, util.GoSumFile))

	suffix := ""
	if gulu.OS.IsWindows() {
		suffix = ".exe"
	}
	executable := filepath.Base(fileDirectory) + suffix
	executable = filepath.Join(fileDirectory, executable)

	// 检查一下contract-sdk-go的版本看看是否需要条件编译指令
	needAddCompileTags := false
	hasImported, importedErr := util.CheckSrcImportChainmakerPackages(fileDirectory)
	if importedErr != nil {
		log.Errorf("build handler check go  file fail, err : %s", importedErr.Error())
		result.Code = -1
		result.Msg = importedErr.Error()
		return
	}
	if hasImported {
		needAddCompileTags = true
	}
	if !needAddCompileTags {
		needCompileNotAddTags, checkErr := util.CheckContractSdkNotAddTag(
			filepath.Join(fileDirectory, util.GoModFile))
		if checkErr != nil {
			log.Errorf("build handler check go mod file fail, err : %s", checkErr.Error())
			result.Code = -1
			result.Msg = checkErr.Error()
			return
		}
		needAddCompileTags = !needCompileNotAddTags
	}

	//编译
	buildStart := time.Now()
	var goBuildArgs []string
	goBuildArgs = append(goBuildArgs, "build", "-o", executable, "-ldflags", "-s -w")
	if needAddCompileTags {
		goBuildArgs = append(goBuildArgs, "--tags", "crypto")
	}
	cmd := exec.Command(runtime.GOROOT()+RealSdkGoRootSuffix+"/bin/go", goBuildArgs...)
	cmd.Dir = fileDirectory
	if global.TestnetEnv != true {
		setCmdEnvWithOsAndArchAndGoRoot(cmd, uid, "", "", os.Getenv("GOPATH"), runtime.GOROOT()+RealSdkGoRootSuffix)
	}

	stdout, stderr, err := util.OpenStdOutAndStdErr(cmd)
	if err != nil {
		log.Errorf("build handler. open stdout and std err fail. uid:%s, err:%v", uid, err)
		result.Code = -1
		return
	}

	if err := cmd.Start(); nil != err {
		log.Errorf("build handler. start run build command fail. uid:%s, err:%v", uid, err)
		result.Code = -1
		return
	}
	log.Debugf("cmd info: path %v,args: %v,env: %v,dir: %v ", cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
	var lines []string
	var wg sync.WaitGroup
	wg.Add(1)

	go sendStdoutDataToWs(sid, &stdout, &wg, executable)
	sendStderrDataToWs(sid, stderr, fileDirectory, &lines, executable)
	defer util.CloseAndLogError(stderr)

	channelRet["cmd"] = "build"
	channelRet["executable"] = executable
	err = cmd.Wait()
	log.Debugf("time cost: build handler, go build 230. (cost:%s)", time.Since(buildStart))

	if err == nil {
		channelRet["nextCmd"] = params["nextCmd"]
		channelRet["output"] = "<span class='build-succ'>" +
			i18n.Get(userConfigInfo.Locale, "check-succ").(string) + "</span>\n"
	} else {
		makeLintsInChRet(channelRet, lines, fileDirectory, userConfigInfo.Locale)
	}
	wg.Wait()

	wsChannel, err := session.GetOutputWS(sid)
	if err != nil {
		log.Warnf("build handler write response data to channel, channel is nil")
		return
	}
	err = wsChannel.WriteJSON(&channelRet)
	if nil != err {
		log.Warnf("build handler warite data to channel fail, err:%v", err)
	} else {
		wsChannel.Refresh()
	}
}

func sendStdoutDataToWs(sid string, stdout *io.ReadCloser, wg *sync.WaitGroup, executable string) {
	defer util.PanicLog()
	defer wg.Done()
	defer util.CloseAndLogError(*stdout)
	//out Reader
	outReader := bufio.NewReader(*stdout)
	channelRetOut := map[string]interface{}{}
	channelRetOut["cmd"] = "build"
	channelRetOut["executable"] = executable
	for {
		wsChannel, err := session.GetOutputWS(sid)
		if err != nil {
			log.Warnf("build handler write stdout to channel, channel is nil, err:%s", err)
			break
		}
		line, err := outReader.ReadString('\n')
		if nil != err {
			if err == io.EOF {
				break
			}
			log.Warnf("build handler send stdout to ws,read data fail. sid:%s, err:%v", sid, err)
			break
		}
		channelRetOut["output"] = line
		err = wsChannel.WriteJSON(&channelRetOut)
		if nil != err {
			log.Warnf("build handler send stdout to ws,write data fail. sid:%s, err:%v", sid, err)
			break
		}
		wsChannel.Refresh()
	}

}

func sendStderrDataToWs(sid string, stderr io.ReadCloser, curDir string, lines *[]string, executable string) {
	defer util.PanicLog()
	errReader := bufio.NewReader(stderr)
	channelRetErr := map[string]interface{}{}
	channelRetErr["cmd"] = "build"
	channelRetErr["executable"] = executable
	for {
		wsChannel, err := session.GetOutputWS(sid)
		if err != nil {
			log.Warnf("build handler write err data to channel, channel is nil")
			break
		}
		line, err := errReader.ReadString('\n')
		if nil != err {
			if err == io.EOF {
				break
			}
			log.Warnf("build handler send stderr to ws,read data fail. sid:%s, err:%v", sid, err)
			break
		}
		*lines = append(*lines, line)
		errOutWithPath := parsePath(curDir, line)
		channelRetErr["output"] = "<span class='stderr'>" + errOutWithPath + "</span>"

		err = wsChannel.WriteJSON(&channelRetErr)
		if nil != err {
			log.Warnf("build handler send stderr to ws,write data fail. sid:%s, err:%v", sid, err)
			break
		}

		wsChannel.Refresh()
	}
}

func sendBuildErrorMessageToWs(uid, sid, message string) {
	defer util.PanicLog()

	channelRetOut := map[string]interface{}{}
	channelRetOut["cmd"] = "build-failed"
	channelRetOut["output"] = "<span class='stderr'> " + message + " </span>"

	wsChannel, err := session.GetOutputWS(sid)
	if err != nil {
		log.Warnf("build all. send build success message to ws, channel is nil. uid:%s, sid:%s", uid, sid)
		return
	}

	err = wsChannel.WriteJSON(&channelRetOut)
	if nil != err {
		log.Warnf("build all. send build success message to ws. uid:%s, sid:%s, err:%v", uid, sid, err)
	}

	wsChannel.Refresh()
}

func makeLintsInChRet(channelRet map[string]interface{}, lines []string, curDir string, locale string) {
	channelRet["output"] = "<span class='build-error'>" + i18n.Get(locale, "check-error").(string) + "</span>\n"
	if len(lines) == 0 {
		return
	}
	// lint process
	if lines[0][0] == '#' {
		lines = lines[1:] // skip the first line
	}

	lints := []*Lint{}
	var err error
	for _, line := range lines {
		if len(line) < 1 || !strings.Contains(line, ":") {
			continue
		}

		if line[0] == '\t' {
			// append to the last lint
			last := len(lints)
			msg := lints[last-1].Msg
			msg += line

			lints[last-1].Msg = msg
			continue
		}

		file := line[:strings.Index(line, ":")]
		left := line[strings.Index(line, ":")+1:]
		index := strings.Index(left, ":")
		lineNo := 0
		msg := left
		if index >= 0 {
			lineNo, err = strconv.Atoi(left[:index])

			if nil != err {
				continue
			}

			msg = left[index+2:]
		}

		lint := &Lint{
			File:     filepath.ToSlash(filepath.Join(curDir, file)),
			LineNo:   lineNo - 1,
			Severity: lintSeverityError,
			Msg:      msg,
		}

		lints = append(lints, lint)
	}
	channelRet["lints"] = lints
}
